package com.sadhoc.router.neighbor;

import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.HashSet;
import com.sadhoc.router.Constants;
import com.sadhoc.router.OnMessageReceiverI;
import com.sadhoc.router.PacketSender;
import com.sadhoc.router.pdu.HDPacket;
import com.sadhoc.router.pdu.HDPacketTypes;
import com.sadhoc.router.pdu.HelloMsg;
import com.sadhoc.router.pdu.TCMsg;
import com.sadhoc.router.topology.SequenceNumber;
import com.sadhoc.router.topology.TopologyTable;
import com.sadhoc.router.util.Debug;
import com.sadhoc.router.util.ExpiringHashMap;

public class NeighborTable extends ExpiringHashMap<String, NeighborTableEntry> {
	
	private static final long serialVersionUID = -5288183977007262698L;
	
	private OnMessageReceiverI recvr;
	private Object lock = new Object();
	private TopologyTable topologyTable;
	private NeighborTableEntry entry;
	private SequenceNumber sequenceNumber;
	private PacketSender s;
	
	// This object contains a list of all symetrical neighbors for quick access
	private HashSet<String> symNeighbors;
	
	/*
	 * Creates a new neighbor table. The topologytable is passed as a reference so that we can remove dead neighbors
	 * from the topology table
	 */
	public NeighborTable(OnMessageReceiverI recvr, TopologyTable topologyTable, PacketSender s) {
		this.recvr = recvr;
		this.topologyTable = topologyTable;
		this.s = s;
		symNeighbors = new HashSet<String>();
		sequenceNumber = new SequenceNumber(0);
	}
	
	/*
	 * Parses and processes hello messages
	 */
	public void handleHelloMsg(String ip, HelloMsg msg) {
	
		NeighborTableEntry entry = null;
		
		// Flag saying this neighbor was JUST added as A-SYM, therefore
		// ignore adding it as a SYM when parsing their node data
		boolean ignore = false;
	
		// If we do not know about this neighbor
		if (getEntry(ip) == null) {				
			// Add an entry with the A-SYM flag
			entry = new NeighborTableEntry(NeighborTableEntry.ASYM);
			addEntry(ip, entry, Constants.NEIGHBOR_TIMEOUT);
			Debug.printBasic("Adding A-SYM neighbor " + ip);
			// Flag saying to ignore 
			ignore = true;
		}
		// We know about this neighbor
		else {
			// Update the timestamp..
			updateTime(ip,  Constants.NEIGHBOR_TIMEOUT);
			// Retrieve the entry from the table
			entry = getEntry(ip);
			// If this neighbor was previously lost, re-add as A-SYM
			if (entry.getNeighborType() == NeighborTableEntry.LOST) {
				entry.setNeighborType(NeighborTableEntry.ASYM);
				Debug.printBasic("Adding A-SYM neighbor " + ip);
				ignore = true;
			}
		}

		/*
		 *  Hello messages contain a list of all of the nodes neighbors.
		 *  We need to search through this. If the hello message contains
		 *  our IP address with the flag SYM set, then that concludes the three
		 *  way handshake
		 *  
		 */
		
		HashMap<String, NeighborTableEntry> nodeData =  msg.getNodes();

		if (nodeData.get(Constants.ip) != null) {
			if ((entry.getNeighborType() != NeighborTableEntry.SYM) && (!ignore)) {
				Debug.printBasic("Setting SYM neighbor " + ip);
				entry.setNeighborType(NeighborTableEntry.SYM);
				setChangedFlag(true);
				sequenceNumber.increase();
				synchronized(lock) {
					symNeighbors.add(ip);
				}
			}
		}
		
		// if we just added this neighbor as A-SYM, lets send a hello message immediately instead of waiting for the next hello
		// advertisement interval to accelerate the process a little bit...
		if (ignore) {
			try {
				Debug.printDetailed("Sending accelerated hello message");
				s.sendUDP(Constants.broadcast, new HDPacket(Constants.broadcast, Constants.ip, HDPacketTypes.UDP, HDPacketTypes.HOCDROUTER_ROUTER_HELLO, generateHelloMsg()));
			} catch (UnknownHostException e) {
				e.printStackTrace();
			}
		}
				
	}
	
	/*
	 * Checks if the neighbor is symetrical
	 */
	public boolean isNeighborSym(String ip) {
		
		synchronized(getLock()) {
			if ((entry = getEntry(ip)) != null)
				if (entry.getNeighborType() == NeighborTableEntry.SYM)
					return true;
		}
		return false;
		
	}
	
	/*
	 * Returns a copy of the symetrical neighbor list
	 */
	public HashSet<String> cloneSymNeighbors() {
		
		synchronized(lock) {
			return new HashSet<String>(symNeighbors);
		}
		
	}
		
	/*
	 * Retrns a new hello PDU with all required data
	 */
	public HelloMsg generateHelloMsg() {
		
		synchronized(getLock()) {
			return new HelloMsg(getCopyOfAllData());
		}
		
	}
	
	/*
	 * Returns a new TC PDY with all required data
	 */
	public TCMsg generateTCMsg() {
		
		synchronized(lock) {
			return new TCMsg(Constants.ip, sequenceNumber, new HashSet<String>(symNeighbors));
		}
		
	}
	
	/*
	 * The ExpiringHashMap class has removed a neighbor because they timed out.
	 * We need to notify the routing protocol that this neighbor was removed
	 * and also need to remove any associated entries from that neighbor
	 */
	@Override
	public void onRemoved(String key) {
		
		// Remove all topology entries from that neighbor
		topologyTable.removeEntriesFromOriginator(key);
		
		// Increment our sequence number value (TC PDU) since there's been a change
		sequenceNumber.increase();
		
		// Remove the neighbor from our symetrical list
		synchronized(lock) {
			symNeighbors.remove(key);
		}
		try {
			Debug.printBasic("Removing neighbor " + (String)key);
			// Notify the router
			recvr.onMessageRecv(new HDPacket((String)key, "0.0.0.0", HDPacketTypes.INTERNAL, HDPacketTypes.HOCDROID_NEIGHBOR_LOST, null));
			Debug.printDetailed("Sending accelerated TC message");
			
			// Immediately send a TC message which contains our incremented sequence number. This will trigger a flood on the network for fast update
			s.sendUDP(Constants.broadcast, new HDPacket(Constants.broadcast, Constants.ip, HDPacketTypes.UDP, HDPacketTypes.HOCDROUTER_ROUTER_TC, generateTCMsg()));
		} catch (UnknownHostException e) {
			e.printStackTrace();
		}
		
	}
	
	/*
	 * This method returns a copy of the neighborTable
	 */
	public NeighborTable clone() {
		
		NeighborTable table = new NeighborTable(null, null, null); 
		synchronized(getLock()) {
			HashMap<String, NeighborTableEntry> x = getCopyOfAllData();
			for (String key : keySet()) {
				NeighborTableEntry e = x.get(key);
				table.addEntry(key, e, this.get(key));
			}
		}
		
		return table;
		
	}
	
	/*
	 * Formats the neighbor table as a string
	 */
	public String toString() {
		String s = "Neighbor           \tType\n";
		s += "-----------\t--------\n";
	
		synchronized(getLock()) {
			HashMap<String, NeighborTableEntry> x = getCopyOfAllData();
			for (String key : keySet()) {
				NeighborTableEntry e = x.get(key);				
				s += key + "\t" + e + "\n";
			}
		}
		return s;
	}
	
}
